﻿// Rob Holme 
// 30/05/2015

using System;
using System.IO;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;

namespace HL7Query  
{
    class Program
    {
        private static bool ignoreWarningsFlag = false;
        private static bool uniqueValuesFlag = false;
        private static HashSet<string> uniqueValuesList = new HashSet<string>();
        private static bool listFilenamesFlag = false;
        private static string messageFilterElement = null;
        private static string messageFilterValue = null;
        private static string hl7ElementLocation;
  

        /// <summary>
        /// Main function. Parse the command line parameters, verify file requested exists etc.
        /// </summary>
        /// <param name="args"></param>
        static void Main(string[] args)
        {
            // look for command line switches
            for (int i = 0; i < args.Length; i++) 
            {
                switch (args[i].ToUpper())
                {
                    // flag to suppress warnings when items do not exit in the HL7 file.
                    case "-IGNOREWARNINGS":
                    case "-IGNORE":
                    case "-I":
                        ignoreWarningsFlag = true; 
                        break;
                    case "-?":
                    case "-H":
                    case "-HELP":
                        Usage();
                        return;
                    // flag to display unique values only
                    case "-UNIQUE":
                    case "-U":
                        uniqueValuesFlag = true; 
                        break;
                    case "-FILENAMES":
                    case "-FILENAME":
                    case "-LISTFILENAMES":
                    case "-L":
                        listFilenamesFlag = true;
                        break;
                    // the use specifies an item to filter for. If the file does not match this value, it is skipped. e.g. 'PID-3[1].1=E1234567' or 'PID-5.2=ROBERT' or 'PV1-2=INPATIENT'
                    case "-FILTER": 
                    case "-F":
                        if (i+1 < args.Length) 
                        {
                            if (Regex.IsMatch(args[i + 1], "^[A-Z]{2}([A-Z]|[0-9])([[]([1-9]|[1-9][0-9])[]])?(([-][0-9]{1,3}([[]([1-9]|[1-9][0-9])[]])?[.][0-9]{1,3}[.][0-9]{1,3})|([-][0-9]{1,3}([[]([1-9]|[1-9][0-9])[]])?[.][0-9]{1,3})|([-][0-9]{1,3}([[]([1-9]|[1-9][0-9])[]])?))?=", RegexOptions.IgnoreCase))
                            {
                                string[] tempString = (args[i+1]).Split('=');
                                messageFilterElement = tempString[0]; // the HL7 segment, field, compent, sub component to filter on.
                                if (tempString.Count() > 1)
                                {
                                    messageFilterValue = tempString[1]; // the value to match when -FILTER switch is specified
                                }
                            }
                            else // the value provided after the -filter switch did not match the expected format of a message trigger.
                            {
                                Console.WriteLine();
                                WriteWarning("The '-Filter' value provided '" + args[i + 1] + "' is not a valid filter string. e.g. PID-3.1=E0123456");
                                Console.WriteLine("Run 'HL7Query.exe -Help' for a description of command line options.");
                                Console.WriteLine("Refer to the online Documentation for further assistance: https://hl7query.codeplex.com/documentation"); 
                                Console.WriteLine();
                                return;
                            }
                        }
                        // a value wasn't provided after the -Filter switch which is required
                        else 
                        {
                             Console.WriteLine(); 
                             WriteWarning("A filter value needs to be provided with the -Filter switch. e.g. '-Filter MSH-9=ADT^A01'");
                             Console.WriteLine("Run 'HL7Query.exe -Help' for a description of command line options.");
                             Console.WriteLine();
                             return;
                        }
                        break;
                }
            }
            
            // valide the command line parameters suppplied, display usage information if not correct.
            if (args.Count() < 2)
                HL7Query.Program.Usage();
            else
            {
                hl7ElementLocation = args[1]; // expect the HL7 element to return to be provided as second command line argument.
                if (IsLH7LocationStringValid(hl7ElementLocation)) // before processing the file, validate the string identifying the location of the HL7 element
                {
                    // only single file provided, process the single file.
                    if (File.Exists(args[0])) 
                    {
                        ProcessFile(args[0]);
                        if (uniqueValuesFlag) 
                        {
                            foreach (string item in uniqueValuesList)
                            {
                                Console.WriteLine(item);
                            }
                        }

                    }
                    // a directory has been specified instead of a file. Process all files in the directory.
                    else if (Directory.Exists(args[0])) 
                    {
                        string[] fileArray = Directory.GetFiles(args[0]);
                        foreach (string fileName in fileArray)
                        {
                            ProcessFile(fileName);
                        }
                        if (uniqueValuesFlag) 
                        {
                            foreach (string item in uniqueValuesList)
                            {
                                Console.WriteLine(item);
                            }
                        }
                    }
                    else
                    {
                        // the path provided is not a valid file or directory.
                        Console.WriteLine();
                        WriteError("The file or directory '" + args[0] + "' does not exist");
                        Console.WriteLine("Run 'HL7Query.exe -Help' for a description of command line options.");
                        Console.WriteLine();
                    }
                }
            }
        }


        /// <summary>
        /// determine if the HL7 element location string is in the correct format. Return false if format issues detected
        /// </summary> 
        static bool IsLH7LocationStringValid(string HL7ItemLocation)
        {
            if (System.Text.RegularExpressions.Regex.IsMatch(HL7ItemLocation, "^[A-Z]{2}([A-Z]|[0-9])([[]([1-9]|[1-9][0-9])[]])?(([-][0-9]{1,3}([[]([1-9]|[1-9][0-9])[]])?[.][0-9]{1,3}[.][0-9]{1,3})|([-][0-9]{1,3}([[]([1-9]|[1-9][0-9])[]])?[.][0-9]{1,3})|([-][0-9]{1,3}([[]([1-9]|[1-9][0-9])[]])?))?$", RegexOptions.IgnoreCase)) // regex to confirm the HL7 element location string is valid
            {
                if (System.Text.RegularExpressions.Regex.IsMatch(HL7ItemLocation, "([.]0)|([-]0)", RegexOptions.IgnoreCase)) // make sure field, component and subcomponent values are not 0. Must be 1 or greater.
                {
                    Console.WriteLine();
                    WriteError("Field, Component, and Sub Component values must be greater than 0. '" + HL7ItemLocation + "' does not identify a valid HL7 element.");
                    Console.WriteLine("Run 'HL7Query.exe -Help' for a description of command line options.");
                    Console.WriteLine();
                    return false;
                }
                return true;        
            }
            Console.WriteLine();
            WriteError("The HL7 item requested '" + HL7ItemLocation + "' does not appear to be valid HL7 reference to a segment, field, component or subcomponent");
            Console.WriteLine("Run 'HL7Query.exe -Help' for a description of command line options.");
            Console.WriteLine();
            return false;
        }


        /// <summary>
        /// Process each HL7 file based on the command line parameters provided.
        /// </summary>
        /// <param name="Filename"></param>
        static void ProcessFile(string filename)
        {
            string[] results;
            bool filterMatchFound = false;
            try
            {

                HL7Document hl7File = new HL7Document(filename);
                if (!hl7File.IsFileValid())
                {
                    WriteError("The file " + filename + " does not appear to be a valid HL7 file.");
                    return;
                }
                // if a filter item has been specified, confirm if the item matches the filter value specified by the user. If no macth found then return without results. TO DO: a search value of "" does not work.
                if (messageFilterElement != null)                  
                {
                    string[] filterResults = null;
                    filterResults = hl7File.GetHL7Item(messageFilterElement);
                    if (filterResults.Count() > 0)
                    {
                        // for repeating items check to see if the filter value provided matches any of the repeating elements. If needed, the user can provide a specific repeat in the filter string.
                        foreach (string filterResult in filterResults)  
                        {
                            if (filterResult.ToUpper() == messageFilterValue.ToUpper())
                            {
                                filterMatchFound = true;
                            }
                       }
                    }
                    else if ((filterResults == null) && (messageFilterValue == null))
                    {
                           filterMatchFound = true; // look for null matrches - i.e. the user deliberatly specied a null value to match on. ie only search in messages not containing a vlaue for a given HL7 element.
                    }
                    if (!filterMatchFound)
                    {
                        return; // no match found, so return without processing the file further.
                    }
                }
                // list the filenames as the files are processed, except if showing unique items only.
                if ((listFilenamesFlag) && !(uniqueValuesFlag)) 
                {
                    WriteInformation(filename);
                }

                results = hl7File.GetHL7Item(hl7ElementLocation);
                if (results == null)
                {
                    WriteWarning("Could not locate a value for " + hl7ElementLocation);
                    return;
                }
                // no results returned from search. Ignore warning if flag set, or if getting unique values.
                if ((results.Count() == 0) && (ignoreWarningsFlag == false) && !(uniqueValuesFlag)) 
                {
                    WriteWarning(" The item '" + hl7ElementLocation + "' was not found");
                }
                else
                {
                    foreach (string result in results)
                    {
                        if (uniqueValuesFlag)
                            uniqueValuesList.Add(result); // if unique switch set, create a list rather than display results immediatly.
                        else
                            Console.WriteLine(result);
                    }
                }
            }

            catch (Exception e) // trap exceptions returned from operations on the HL7 file. Such as access deniet etc. Display exception to the user, resume from next file.
            {
                WriteError("An exception has ocurred. " + e.Message);
            }
        }


        /// <summary>
        /// Display help message to the console.
        /// </summary>
        static void Usage()
        {
            Console.WriteLine("");
            Console.WriteLine("HL7Query - v1.1 - Robert Holme.  Query HL7 files to return specific segments, fields, components or subcomponents.");
            Console.WriteLine("Usage:");
            Console.WriteLine("");
            Console.ForegroundColor = ConsoleColor.Yellow;
            Console.WriteLine("HL7Query.exe <filename> <HL7Element> [-IgnoreWarnings] [-ListFilenames] [-Unique] [-Filter <HL7Element=Value>]");
            Console.ResetColor();
            Console.WriteLine("");
            Console.WriteLine("    <filename> is the path to a single HL7 file, or a folder of multiple HL7 files");
            Console.WriteLine("");
            Console.WriteLine("    <HL7Element> identifes the HL7 element. Formatted as <Segment>-[Field].[Component].[SubComponent]");
            Console.WriteLine("         <Segment> is the 3 letter segment code. Field, Component and SubComonent are identifed by number.");
            Console.WriteLine("         e.g 'PID' or 'PID-3' or 'PID-3.1.1'. A specific repeat value can be provided for segments and/or fields. e.g. PID-3[2] would");
            Console.WriteLine("         return the second ocurance of the PID-3 field. Like wise NK1[2]-5 would return the NK1-5 field from the second ocurrance of the");
            Console.WriteLine("         NK1 segment. NK1[2]-5[1] would be first repeat of the NK1-5 field from the second repeat of the NK1 segment.");
            Console.WriteLine("");
            Console.WriteLine("    [-IgnoreWarnings] | [-I] is an optional switch to suppress warnings when a segment, field, component or");
            Console.WriteLine("         subcomponent is not present in the message.");
            Console.WriteLine("");
            Console.WriteLine("    [-ListFilenames] | [-L] is an optional switch list the name associated with the results returned.");
            Console.WriteLine("");
            Console.WriteLine("    [-Unique] | [-U] display unique results only. If -ListFileNames is also supplied it will be ignored.");
            Console.WriteLine("          Warnings will also be suppressed. ");
            Console.WriteLine("");
            Console.WriteLine("    [-Filter <HL7Element=Value>] | [-F <HL7Element=Value>] Only search messages where the HL7Element specified");
            Console.WriteLine("          matches the value provided.");
            Console.WriteLine("         e.g. '-Filter MSH-9=ADT^A01' will only search messages with a trigger of 'ADT^A01' ");
            Console.WriteLine("         If <HL7Element> identifies multiple items in a file (ie repeating items, only the first value will be");
            Console.WriteLine("         used for the filter. You can specify a specif repeat number if needed. e.g 'PID-3[2]=E1234567' will only");
            Console.WriteLine("         examine the second repeat of PID-3");
            Console.WriteLine("         Exact match only. If a value isn't provided (e.g. 'PV1-19=', it will match empty or missing HL7Elements.");
            Console.WriteLine("         For rurther information refer to the online documentation: https://hl7query.codeplex.com/documentation");
            Console.WriteLine("");
            Console.WriteLine("Example: HL7Query.exe c:\\HL7 Files\\ PID-3.1 -ListFilenames -Filter ADT^A08");
            Console.WriteLine("Example: HL7Query.exe c:\\HL7 Files\\ PID-3.1 -L -F ADT^A08");
            Console.WriteLine("Example: HL7Query.exe c:\\HL7 Files\\MDM_D4923.txt TXA-2");
            Console.WriteLine("Example: HL7Query.exe c:\\HL7 Files\\ PID-42.1.1 -Unique");
            Console.WriteLine("");
        }

        
        /// <summary>
        /// Display information message to the console. Dark green text on existing background.
        /// </summary>
        /// <param name="InformationMessage"></param>
        static void WriteInformation(string InformationMessage)
        {
            Console.ForegroundColor = ConsoleColor.DarkGreen;
            Console.WriteLine(InformationMessage);
            Console.ResetColor();
        }


        /// <summary>
        /// Display a warning message to the console formmated in Yellow text on existing background.
        /// </summary>
        /// <param name="WarningMessage"></param>
        static void WriteWarning(string WarningMessage)
        {
            Console.ForegroundColor = ConsoleColor.Yellow;
            Console.WriteLine("WARNING: " + WarningMessage);
            Console.ResetColor();
        }


        /// <summary>
        /// Display a error message to the console formmated in red text on a black background.
        /// </summary>
        /// <param name="ErrorMessage"></param>
        static void WriteError(string ErrorMessage)
        {
            Console.ForegroundColor = ConsoleColor.Red;
            Console.BackgroundColor = ConsoleColor.Black;
            Console.WriteLine("ERROR: " + ErrorMessage);
            Console.ResetColor();
        }
    }

}
